﻿namespace IBTrader.Modules.Market
{
    using AxTWSLib;
    using IBTrader.Model;
    using IBTrader.Modules.Configuration;
    using IBTrader.Modules.External;
    using IBTrader.Modules.Read;
    using IBTrader.Modules.Tws;
    using NLog;
    using System;
    using System.Linq;
    using System.Windows;

    /*         
         *      Filled - indicates that the order has been completely filled.
         *      PendingSubmit - indicates that you have transmitted the order, but have not yet received confirmation that it has been accepted by the order destination. NOTE: This order status is not sent by TWS and should be explicitly set by the API developer when an order is submitted.
         *      PendingCancel - indicates that you have sent a request to cancel the order but have not yet received cancel confirmation from the order destination. At this point, your order is not confirmed canceled. You may still receive an execution while your cancellation request is pending. NOTE: This order status is not sent by TWS and should be explicitly set by the API developer when an order is canceled.
         *      Cancelled - indicates that the balance of your order has been confirmed canceled by the IB system. This could occur unexpectedly when IB or the destination has rejected your order.
         *      Inactive - indicates that the order has been accepted by the system (simulated orders) or an exchange (native orders) but that currently the order is inactive due to system, exchange or other issues.
         *      Submitted - indicates that your order has been accepted at the order destination and is working.
         *      PreSubmitted - indicates that a simulated order type has been accepted by the IB system and that this order has yet to be elected. The order is held in the IB system until the election criteria are met. At that time the order is transmitted to the order destination as specified .
         *      ApiCanceled - after an order has been submitted and before it has been acknowledged, an API client client can request its cancelation, producing this state.
*/
    public enum OrderStatus { Undefined, Filled, PendingSubmit, PendingCancel, Cancelled, Inactive, Submitted, PreSubmitted, ApiCanceled, Placed }
    public enum OrderingStatus { None = 0, Buying, Bought, Selling }
    public enum OrderingType { Enter, Exit, Trail, End }
    class Order
    {
        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
        private int nextId;
        private readonly IIB ib;
        private readonly Store store;
        private static LogLevel logLevel = LogLevel.Trace;
        public event EventHandler<OrderLine> Buy, Sell;
        public Order(IIB ib, Store store)
        {
            this.ib = ib;
            this.store = store;
            //ib.Connected += (_, __) => logLevel = LogLevel.Debug;
        }

        public void NextValidId(object _, _DTwsEvents_nextValidIdEvent e)
        {
            nextId = e.id;
        }
        public void Update(object _, _DTwsEvents_orderStatusEvent e)
        {
            var order = ib.IsConnected() ? store.Orders.SingleOrDefault(r => r.Id == e.id) : store.Orders[e.id]; // performance: backlog idx==id
            logger.Log(logLevel, "orderStatus id: {0} {1} {2} {3} order: {4}", e.id, e.avgFillPrice, e.filled, e.status, order);
            if (order == null) return;
            order.FillPrice = e.avgFillPrice;
            order.Filled = e.filled;
            UpdateOrderStatus(order, (OrderStatus)Enum.Parse(typeof(OrderStatus), e.status));
        }
        public void Update(object _, ExecDetailsEventArgs e)
        {
            var execution = e.execution as IBApi.Execution;
            var order = store.Orders.SingleOrDefault(o => o.Id == execution.OrderId);
            logger.Log(logLevel, "execDetails id: {0} {1} {2} order: {3}", execution.OrderId, execution.CumQty, execution.AvgPrice, order);
        }
        public void CancelPending(object _, EventArgs __)
        {
            if (!ib.IsConnected()) return; // not needed in backlog mode

            foreach (var order in store.Orders.Where(o =>
                   o.Status != OrderStatus.Filled
                && o.Status != OrderStatus.Cancelled
                && DateTime.Now.TimeOfDay - o.Created.TimeOfDay > o.Algo.OrderDuration
                ).ToArray())
            {
                logger.Debug("CancelPending {0}", order);
                UpdateOrderStatus(order, OrderStatus.PendingCancel);
                ib.cancelOrder(order.Id);
            }
        }
        public void Error(object sender, _DTwsEvents_errMsgEvent e)
        {
            switch (e.errorCode)
            {
                case 110: // The price does not conform to the minimum price variation for this contract.
                case 200: // No security definition has been found for the request.
                case 201: // Order rejected - reason:The order has already expired. : 
                    var order = store.Orders.SingleOrDefault(o => o.Id == e.id);
                    logger.Debug("CancelInvalid order [{0}]", order);
                    if (order!=null)
                        UpdateOrderStatus(order, OrderStatus.Cancelled);
                    break;
            }
        }
        private void UpdateOrderStatus(Model.Order order, OrderStatus newStatus)
        {
            //if (Application.Current == null) return;
            //Application.Current.Dispatcher.Invoke((Action<Model.Order, OrderStatus>)Status.Update, order, newStatus);
            Status.Update(order, newStatus);
        }
        public void End(object _, EventArgs args) // manual, timer or simulation
        {
            foreach (var algo in store.Algos)
            {
                if (args is Line || args is EndArgs) // simulation or manual end
                {
                    algo.TradingCancelled = true;
                    logger.Debug("Trade end {0}", args);
                }
                if (!IsTradingEnabled(algo, TimeSpan.Zero))
                {
                    var pending = Calculate.Pending(algo);
                    if (pending.Quantity != 0)
                    {
                        logger.Debug("Bought {0} Sold {1}", algo.Orders.Bought, algo.Orders.Sold);
                        var line = args as Line;
                        var order = line == null ? new OrderLine { Algo = algo } : new OrderLine { Algo = algo, DateTime = line.DateTime, Price = line.Price };
                        Place(order, OrderingType.End, pending.Action, OrderType.MKT, pending.Quantity);
                    }
                }
                //if ((args is Timer.TimerArgs) && !IsTradingEnabled(algo))
                //{
                //    algo.TradingCancelled = false; // reset 
                //}
            }
        }
        public void Enter(object _, OrderLine order)
        {
            Place(order, OrderingType.Enter, order.Algo.OrderType, () => order.Algo.OrderingStatus == OrderingStatus.None);
        }
        public void Exit(object _, OrderLine order)
        {
            Place(order, OrderingType.Exit, order.Algo.OrderType, () => order.Algo.OrderingStatus == OrderingStatus.Bought);
        }       
//        internal void Trail(Store.ContractRow contract, IB.OrderType orderType, bool up)
//        {
//            Place(contract, up, 0, OType.Trail, orderType, () => contract.OrderStatus == (int)OStatus.Bought);
//        }
        private void Place(OrderLine order, OrderingType type, OrderType orderType, Func<bool> validStatus)
        {
            if (!validStatus() || !IsTradingEnabled(order.Algo, order.DateTime.TimeOfDay)) return;
            if (order.Algo.Contract.MinTick != 0)
                order.Price = (double)(((int)(order.Price)) + ((int)(((decimal)order.Price % 1m) / order.Algo.Contract.MinTick)) * order.Algo.Contract.MinTick);
            var action = order.Up ? OrderAction.BUY : OrderAction.SELL;
            var quantity = Calculate.Quantity(action, order.Algo);
            if (quantity <= 0) return;
            Place(order, type, action, orderType, quantity);
        }
        private void Place(OrderLine order, OrderingType oType, OrderAction action, OrderType iType, int quantity)
        {
            var contract = order.Algo.Contract;
            var orderRef = DateTime.Now.Ticks.ToString();
            var id = ib.IsConnected() ? nextId++ : store.Orders.Count;
            var iOrder = ib.CreateOrder(action, iType, order.Price, quantity, orderRef, order.Algo.OrderDuration);
            var iContract = ib.CreateContract(contract.Symbol, contract.SecurityType, contract.Exchange, contract.Currency, contract.Multiplier, contract.PrimaryExchange);
            var dOrder = new Model.Order(store, OrderStatus.Submitted) { Id = id, Created = order.DateTime, AlgoId = order.Algo.Id, Action = action, Type = oType, 
                OrderType = iType, Price = order.Price, Quantity = quantity, OrderRef = orderRef };
            var call = action == OrderAction.BUY ? Buy : Sell;
            if (oType != OrderingType.End && call != null) call(this, order);
            order.Algo.OrderingStatus = Status.Next(order.Algo.OrderingStatus);
            //if (Application.Current == null) return;
            //Application.Current.Dispatcher.Invoke((Action)(() => {
                store.Orders.Add(dOrder);
                if (ib.IsConnected())
                    ib.placeOrder(id, iContract, iOrder);
            //}));
            logger.Log(logLevel, "PlaceOrder {0}", dOrder);
        }
        private bool IsTradingEnabled(Algo algo, TimeSpan ts) 
        {
            if (algo.TradingCancelled) return false;
            if (ts == TimeSpan.Zero) // End
            {
                if (!ib.IsConnected()) return true; // test mode
                ts = Times.Now.TimeOfDay;           
            }
            return Times.InRange(algo.TradingStart, algo.TradingEnd, ts);
        }
        private static class Calculate
        {
            internal class PendingOrder
            {
                public OrderAction Action { get; set; }
                public int Quantity { get; set; }
            }
            internal static PendingOrder Pending(Algo algo)
            {
                var diff = Difference(algo);
                var action = diff > 0 ? OrderAction.SELL : OrderAction.BUY;
                var quantity = Math.Abs(diff);
                return new PendingOrder { Action = action, Quantity = quantity };
            }
            internal static int Difference(Algo algo)
            {
                var total = algo.Orders.Total;
                var bought = algo.Orders.Bought;
                var sold = algo.Orders.Sold;
                var diff = bought - sold;
                //var msg = string.Format("total {0} bought {1} sold {2} diff {3}", total, bought, sold, diff);
                //logger.Trace(msg);
                return diff;
            }
            internal static int Quantity(OrderAction action, Algo algo)
            {
                var diff = Difference(algo);
                var quantity = action == OrderAction.BUY ? -diff : +diff;

                if (quantity == 0 || quantity > algo.OrderSize)
                    quantity = algo.OrderSize;

                return quantity;
            }
        }
        private static class Status
        {
            private static readonly Logger logger = LogManager.GetCurrentClassLogger();
            internal static void Update(Model.Order order, OrderStatus newStatus)
            {
                if (order.Status == newStatus) return;
                if (order.Status == OrderStatus.Cancelled && newStatus == OrderStatus.Inactive)
                {
                    logger.Debug("Skip {0} => {1}", order.Status, newStatus);
                    return;
                }
                logger.Log(logLevel, "{0} => {1}", order, newStatus);
                order.Status = newStatus;
                //if (order.Algo.OrderingStatus != OrderingStatus.Buying && order.Algo.OrderingStatus != OrderingStatus.Selling)
                //    return;

                order.Algo.OrderingStatus = Calculate(order, newStatus);
            }
            private static OrderingStatus Calculate(Model.Order order, OrderStatus newStatus)
            {
                var exitPending = (order.Type == OrderingType.Exit || order.Type == OrderingType.Trail) && Order.Calculate.Quantity(order.Action, order.Algo) > 0;

                var filled1 = newStatus == OrderStatus.Filled;
                var cancel1 = newStatus == OrderStatus.Cancelled && order.Filled == 0;
                var filled2 = newStatus == OrderStatus.Cancelled && order.Filled > 0 && !exitPending;
                var cancel2 = newStatus == OrderStatus.Cancelled && order.Filled > 0 && exitPending;
                //logger.Debug("{0} {1} {2} {3} {4}", order, filled1, filled2, cancel1, cancel2);
                if (filled1 || filled2) return Next(order.Algo.OrderingStatus);
                if (cancel1 || cancel2) return Prev(order.Algo.OrderingStatus);
                return order.Algo.OrderingStatus;
            }
            internal static Func<OrderingStatus, OrderingStatus> Next = status => Value(+1, status);
            private static Func<OrderingStatus, OrderingStatus> Prev = status => Value(-1, status);
            private static Func<int, OrderingStatus, OrderingStatus> Value = (off, src) =>
            {
                var dst = (OrderingStatus)(((int)src + off) % Enum.GetValues(typeof(OrderingStatus)).Length);
                logger.Log(logLevel, "Status {0} => {1}", src, dst);
                return dst;
            };
        }
        public class EndArgs : EventArgs
        {
            public new readonly static EndArgs Empty = new EndArgs();
        }
    }
}
